﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Text;

namespace PacManLib
{
    /// <summary>
    /// Manages a game of Pac-Man. 
    /// Holds the board, the score and helps manage the movement
    /// of the players.
    /// </summary>
    public class Game
    {
        #region Events

        /// <summary>
        /// Data passed with events raised by this class.
        /// (Even if no args are needed, using this means that we can use
        /// the standard EventHandler, and standard event signature.)
        /// </summary>
        public class Args : EventArgs
        {
        }

        /// <summary>
        /// Event raised when a pellet is eaten.
        /// </summary>
        public event EventHandler<Args> PelletEatenEvent;

        /// <summary>
        /// Event raised when a power-pellet is eaten.
        /// </summary>
        public event EventHandler<Args> PowerPelletEatenEvent;

        /// <summary>
        /// Event raised when Pac-Man kills a ghost.
        /// </summary>
        public event EventHandler<Args> GhostKilledEvent;

        /// <summary>
        /// Event raised when Pac-Man has been killed.
        /// </summary>
        public event EventHandler<Args> PacManKilledEvent;

        /// <summary>
        /// Event raised when no ghosts are scared after a period 
        /// when they were.
        /// </summary>
        public event EventHandler<Args> GhostsNoLongerScaredEvent;

        /// <summary>
        /// Event raised when the game is over.
        /// </summary>
        public event EventHandler<Args> GameOverEvent;

        #endregion

        #region Public methods and properties

        /// <summary>
        /// Constructor.
        /// </summary>
        public Game()
        {
            initializeNames();
            resetGhostScores();
            countPellets();
            findTileVisibility();
        }

        /// <summary>
        /// Gets the Pac-Man AI.
        /// </summary>
        public IPacManAI getPacManAI()
        {
            return m_pacManAI;
        }

        /// <summary>
        /// Sets the Pac-Man AI.
        /// </summary>
        public void setPacManAI(IPacManAI ai)
        {
            try
            {
                m_pacManAI = ai;
                m_pacManAIName = Utility.getAILongName(ai);
            }
            catch (Exception ex)
            {
                Log.log(String.Format("Error getting AI name, error={0}", ex.Message));
            }
        }

        /// <summary>
        /// Gets the ghost AI for the index passed in.
        /// </summary>
        public IGhostAI getGhostAI(int index)
        {
            return m_ghostAIs[index];
        }

        /// <summary>
        /// Sets the ghost AI for the index passed in.
        /// </summary>
        public void setGhostAI(int index, IGhostAI ai)
        {
            try
            {
                m_ghostAIs[index] = ai;
                m_ghostAINames[index] = Utility.getAILongName(ai);
            }
            catch (Exception ex)
            {
                Log.log(String.Format("Error getting AI name, error={0}", ex.Message));
            }
        }

        /// <summary>
        /// Gets the board.
        /// </summary>
        public Board Board
        {
            get { return m_board; }
        }

        /// <summary>
        ///  Gets the Pac-Man's score.
        /// </summary>
        public int getPacManScore()
        {
            return m_pacManScore;
        }

        /// <summary>
        /// Returns the number of stopwatch tick taken by the Pac-Man
        /// Ai when playing turns.
        /// </summary>
        public long getPacManTicks()
        {
            return m_pacManTicks;
        }

        /// <summary>
        /// Gets the score for the ghost requested (zero-based index).
        /// </summary>
        public int getGhostScore(int index)
        {
            return m_ghostsScores[index];
        }

        /// <summary>
        /// Gets the time taken by the ghost requested (zero-based index).
        /// </summary>
        public long getGhostTicks(int index)
        {
            return m_ghostsTicks[index];
        }

        /// <summary>
        /// Gets the number of lives remaining for Pac-Man.
        /// </summary>
        public int getPacManLivesRemaining()
        {
            return m_pacManLivesRemaining;
        }

        /// <summary>
        /// Plays one turn of the game. 
        /// </summary><remarks>
        /// Players may get a chance to move, points may be scored, 
        /// collisions detected and so on.
        /// </remarks>
        public void playOneTurn()
        {
            // We check that we've got all the AIs...
            checkAIsAreSetUp();

            // We record the positions of the players before any movement
            // has taken place. (This is useful for collision detection.)
            storePlayerPositions();

            // We move the Pac-Man and the ghosts...
            movePlayers();

            // We check if Pac-Man has eaten a pellet...
            checkPellets();

            // We check for collisions...
            checkCollisions();

            // If Pac-Man is powered up, we check the time remaining...
            checkPowerUpTimeRemaining();

            // We check if the ghosts are still scared after eating a power-pellet...
            checkGhostsScared();

            // We check if the maximum number of turns has been played...
            m_turnsRemaining--;
            if (m_turnsRemaining == 0)
            {
                Utility.raiseEvent(GameOverEvent, null, null);
            }
        }

        #endregion

        #region Private functions

        /// <summary>
        /// Checks if the ghosts are still scared.
        /// </summary>
        private void checkGhostsScared()
        {
            if (m_ghostsScared == false)
            {
                return;
            }

            // We think that the ghosts are scared, but are they?
            int scaredGhosts = 0;
            foreach (PlayerInfo ghostInfo in m_board.Ghosts)
            {
                if (ghostInfo.IsScared) scaredGhosts++;
            }

            if (scaredGhosts == 0)
            {
                Utility.raiseEvent(GhostsNoLongerScaredEvent, this, null);
                m_ghostsScared = false;
            }
        }

        /// <summary>
        /// We set the initial values of the AI names.
        /// </summary>
        private void initializeNames()
        {
            m_pacManAIName = "";
            for (int i = 0; i < Board.NumberOfGhosts; ++i)
            {
                m_ghostAINames[i] = "";
            }
        }

        /// <summary>
        /// Checks if Pac-Man is powered up, and if so decreases the remaining time.
        /// </summary>
        private void checkPowerUpTimeRemaining()
        {
            if (m_board.PowerUpTurnsRemaining > 0)
            {
                m_board.PowerUpTurnsRemaining--;
                if (m_board.PowerUpTurnsRemaining == 0)
                {
                    // The power-up time has run out, the ghosts are 
                    // no longer scared...
                    foreach (PlayerInfo ghost in m_board.Ghosts)
                    {
                        ghost.IsScared = false;
                    }
                }
            }
        }

        /// <summary>
        /// Checks for collisions. Updates the score and raises an
        /// event if so.
        /// </summary>
        private void checkCollisions()
        {
            // We need to check for two types of collision:
            // 1. Pac-Man and ghost on the same tile.
            // 2. Pac-Man and ghost "passed in the night"

            // Here's what type 2 collisions mean:
            // This can happen if Pac-Man and a ghost were on adjacent tiles 
            // but moving in opposite directions, for example:
            // a) ..PG..
            // b) ..GP..
            // Because of the grid-based and turn-based nature of the framework
            // players can pass by in this way. They are never on the same tile, 
            // but we do need to count this as a collision.

            Point pacManPosition = m_board.PacMan.Position;
            bool collisionOccurred = false;
            int ghostIndex = -1;

            // We loop through collection of ghosts, checking to see
            // if we have collided with each one...
            for (int i = 0; i < Board.NumberOfGhosts; ++i)
            {
                PlayerInfo ghostInfo = m_board.Ghosts[i];

                // 1. Are we on the same tile as this ghost?
                if (pacManPosition == ghostInfo.Position)
                {
                    // We are on the same tile...
                    collisionOccurred = true;
                    ghostIndex = i;
                    break;
                }

                // 2. We see if Pac-Man and this ghost have swapped positions
                //    since the start of the turn...
                Point ghostPositionAfterTurn = ghostInfo.Position;
                Point pacManPositionAfterTurn = m_board.PacMan.Position;
                if (m_pacManPositionBeforeTurn == ghostPositionAfterTurn
                    &&
                    pacManPositionAfterTurn == m_ghostPositionsBeforeTurn[i])
                {
                    // There has been a "collision" of type 2...
                    collisionOccurred = true;
                    ghostIndex = i;
                    break;
                }
            }

            // If a collision has occured, what happens depends on whether 
            // Pac-Man is powered up or not...

            if (collisionOccurred 
                && 
                m_board.Ghosts[ghostIndex].IsScared == false)
            {
                // Pac-Man is not powered-up, so he has lost a life.

                // We decrease the number of lives, and reset the players
                // to their original positions...
                m_pacManLivesRemaining--;
                m_board.resetPacManPosition();
                m_board.resetGhostPositions();

                // All ghosts score when Pac-Man is killed...
                for (int i = 0; i < Board.NumberOfGhosts; ++i)
                {
                    m_ghostsScores[i] += GHOST_KILLS_PACMAN_SCORE;
                }

                // And the ghost that killed Pac-Man gets a bonus...
                m_ghostsScores[ghostIndex] += GHOST_KILLS_PACMAN_BONUS_SCORE;

                // We raise an event t say that Pac-Man has lost a life...
                Utility.raiseEvent(PacManKilledEvent, this, null);

                // If all Pac-Man's lives are gone, we raise event...
                if (m_pacManLivesRemaining == 0)
                {
                    // The ghosts get a bonus of the number 
                    // of turns remaining...
                    for(int i=0; i<Board.NumberOfGhosts; ++i)
                    {
                        m_ghostsScores[i] += m_turnsRemaining;
                    }
                    Utility.raiseEvent(GameOverEvent, this, null);
                }
            }

            if (collisionOccurred
                &&
                m_board.Ghosts[ghostIndex].IsScared == true)
            {
                // Pac-Man is powered up, so he eats the ghost...

                // Pac-Man scores some points and the ghost goes back 
                // to the center...
                m_pacManScore += PAC_MAN_KILLS_GHOST_SCORE;
                m_board.resetGhostPosition(ghostIndex);

                // The ghost is not scared any more...
                m_board.Ghosts[ghostIndex].IsScared = false;

                // We raise an event...
                Utility.raiseEvent(GhostKilledEvent, this, null);
            }
        }

        /// <summary>
        /// Checks if Pac-Man has eaten a pellet. Updates the score and
        /// raises an event if he has.
        /// </summary>
        private void checkPellets()
        {
            // We see if the Pac-Man has eaten a pellet...
            Point pacManPosition = m_board.PacMan.Position;
            Board.Item itemAtPacManPosition = m_board.Items[pacManPosition.X, pacManPosition.Y];
            if (itemAtPacManPosition == Board.Item.Pellet)
            {
                // We remove the pellet from the board, increase the score
                // and raise an event...
                m_pelletsRemaining--;
                m_board.Items[pacManPosition.X, pacManPosition.Y] = Board.Item.Empty;
                m_pacManScore += PELLET_SCORE;
                Utility.raiseEvent(PelletEatenEvent, this, null);
            }

            // We check to see if Pac-Man has eaten a power-pellet...
            if (itemAtPacManPosition == Board.Item.PowerPellet)
            {
                // We remove the pellet from the board and increase the score...
                m_pelletsRemaining--;
                m_board.Items[pacManPosition.X, pacManPosition.Y] = Board.Item.Empty;
                m_pacManScore += POWER_PELLET_SCORE;

                // We set the power-up time. This notifies players that Pac-Man
                // is powered-up...
                m_board.PowerUpTurnsRemaining = POWER_UP_TURNS;

                // We mark all the ghosts as scared...
                foreach (PlayerInfo ghost in m_board.Ghosts)
                {
                    ghost.IsScared = true;
                }
                m_ghostsScared = true;

                // We raise an event...
                Utility.raiseEvent(PowerPelletEatenEvent, this, null);
            }

            // If there are no pellets left, the game is over...
            if (m_pelletsRemaining == 0)
            {
                // Pac-Man gets a bonus based on the number of turns and lives remaining...
                m_pacManScore += m_turnsRemaining;
                m_pacManScore += (m_pacManLivesRemaining * 150);
                Utility.raiseEvent(GameOverEvent, this, null);
            }
        }

        /// <summary>
        /// Moves the Pac-Man and the ghosts.
        /// </summary>
        private void movePlayers()
        {
            // Note that when we pass data to the AIs, we pass copies of the 
            // board and player-info. This prevents the AIs from cheating by 
            // editing the properties of the board, positions etc.

            // We find the direction for the Pac-Man, and move it...
            try
            {
                m_stopwatch.Reset();
                m_stopwatch.Start();
                m_board.PacMan.DesiredDirection = m_pacManAI.getDesiredDirection(m_board.createCopy());
                m_stopwatch.Stop();
                m_pacManTicks += m_stopwatch.ElapsedTicks;
            }
            catch (Exception ex)
            {
                Log.log(String.Format("Error getting Pac-Man direction AI={0}, error={1}", m_pacManAIName, ex.Message));
                m_board.PacMan.DesiredDirection = PlayerInfo.Direction.NoChange;
            }
            m_board.PacMan.movePlayer(m_board);

            // We find the direction for each ghost, and move them...
            for (int i = 0; i < Board.NumberOfGhosts; ++i)
            {
                PlayerInfo ghostInfo = m_board.Ghosts[i];
                IGhostAI ai = m_ghostAIs[i];

                // We create a copy of the board to pass to the ghost.
                // The ghost can only see items in its line of sight.
                Board board = getBoardForGhost(ghostInfo);

                // If the ghost is scared, it moves at half speed...
                if (ghostInfo.IsScared == false
                    ||
                    (ghostInfo.IsScared == true && m_turnsRemaining % 2 == 0))
                {
                    try
                    {
                        m_stopwatch.Reset();
                        m_stopwatch.Start();
                        ghostInfo.DesiredDirection = ai.getDesiredDirection(board, ghostInfo.createCopy());
                        m_stopwatch.Stop();
                        m_ghostsTicks[i] += m_stopwatch.ElapsedTicks;
                    }
                    catch (Exception ex)
                    {
                        Log.log(String.Format("Error getting ghost direction AI={0}, error={1}", m_ghostAINames[i], ex.Message));
                        ghostInfo.DesiredDirection = PlayerInfo.Direction.NoChange;
                    }

                    // We move the ghost. Ghosts always have to move, so if the
                    // ghost's desired direction keeps it stuck against a wall, we
                    // force it to move by turning it through 90-degrees...
                    Point positionBeforeMove = ghostInfo.Position;
                    ghostInfo.movePlayer(m_board);

                    Point positionAfterMove = ghostInfo.Position;
                    if (positionBeforeMove == positionAfterMove)
                    {
                        ghostInfo.unstickGhost(m_board);
                    }
                }
            }
        }

        /// <summary>
        /// Returns a copy of the board, with items visible by the ghost passed in.
        /// </summary>
        private Board getBoardForGhost(PlayerInfo ghostInfo)
        {
            Board board = m_board.createCopy();
            Board.Item[,] items = board.Items;

            // We find the collection of points visible to the ghost...
            HashSet<Point> visibleTiles = m_mapTileVisibility[ghostInfo.Position];
            for (int x = 0; x < Board.Width; ++x)
            {
                for (int y = 0; y < Board.Height; ++y)
                {
                    Board.Item item = items[x, y];
                    if(item == Board.Item.Wall || item == Board.Item.Empty)
                    {
                        // We leave walls and empty tiles alone...
                        continue;
                    }

                    // The tile is a pellet, so we check if it's visible...
                    Point tile = new Point(x, y);
                    if (visibleTiles.Contains(tile) == false)
                    {
                        // The tile is not visible, so we set it to be empty...
                        items[x, y] = Board.Item.Empty;
                    }
                }
            }

            // Is the Pac-Man visible to this ghost?
            if (visibleTiles.Contains(board.PacMan.Position) == false)
            {
                board.PacMan.Position = Board.UnknownPacmanPosition;
            }

            return board;
        }

        /// <summary>
        /// Throws an exception if the AIs are not all set up.
        /// </summary>
        private void checkAIsAreSetUp()
        {
            if (m_pacManAI == null)
            {
                throw new Exception("Pac-Man AI not set up");
            }

            for (int i = 0; i < m_ghostAIs.Length; ++i)
            {
                if (m_ghostAIs[i] == null)
                {
                    throw new Exception("Ghost AI " + i + " not set up.");
                }
            }
        }

        /// <summary>
        /// Stores the player positions.
        /// </summary><remarks>
        /// This is called before players' turns are played. We need
        /// to know this to check if players have "collided" by moving 
        /// past each other. See the checkCollisions() function for
        /// more details.
        /// </remarks>
        private void storePlayerPositions()
        {
            m_pacManPositionBeforeTurn = m_board.PacMan.Position;
            for(int i=0; i<Board.NumberOfGhosts; ++i)
            {
                m_ghostPositionsBeforeTurn[i] = m_board.Ghosts[i].Position;
            }
        }

        /// <summary>
        /// Fills in the collection of which tiles are visible from 
        /// which other tiles.
        /// </summary>
        private void findTileVisibility()
        {
            // We loop through all tiles on the board, finding the collection
            // of other tiles that are visible from it...
            for (int x = 0; x < m_board.Width; ++x)
            {
                for (int y = 0; y < m_board.Height; ++y)
                {
                    findTileVisibility(x, y);
                }
            }
        }

        /// <summary>
        /// Finds the collection of tiles that are visibile from the
        /// tile passed in, and adds them to the collection held by 
        /// this object.
        /// </summary>
        private void findTileVisibility(int x, int y)
        {
            // Is the tile a playable tile, ie not a wall?
            if (m_board.Items[x, y] == Board.Item.Wall)
            {
                return;
            }

            int minX = 0;
            int maxX = m_board.Width - 1;
            int minY = 0;
            int maxY = m_board.Height - 1;

            // We create a set of tiles visible from this one, and add
            // itself in (as all tiles are deemed to be visible from themself)...
            HashSet<Point> visibleTiles = new HashSet<Point>();
            Point thisPoint = new Point(x, y);
            visibleTiles.Add(thisPoint);

            // We look left...
            int offset = -1;
            while (x + offset >= minX && m_board.Items[x + offset, y] != Board.Item.Wall)
            {
                visibleTiles.Add(new Point(x + offset, y));
                offset--;
            }

            // We look right...
            offset = 1;
            while (x + offset <= maxX && m_board.Items[x + offset, y] != Board.Item.Wall)
            {
                visibleTiles.Add(new Point(x + offset, y));
                offset++;
            }

            // We look up...
            offset = -1;
            while (y + offset >= minY && m_board.Items[x, y + offset] != Board.Item.Wall)
            {
                visibleTiles.Add(new Point(x, y + offset));
                offset--;
            }

            // We look down...
            offset = 1;
            while (y + offset <= maxY && m_board.Items[x, y + offset] != Board.Item.Wall)
            {
                visibleTiles.Add(new Point(x, y + offset));
                offset++;
            }

            // We add the collection of visible tiles to the map...
            m_mapTileVisibility.Add(thisPoint, visibleTiles);
        }

        /// <summary>
        /// Sets all the ghosts' scores to zero.
        /// </summary>
        private void resetGhostScores()
        {
            // We set the ghost scores to zero...
            for (int i = 0; i < m_ghostsScores.Length; ++i)
            {
                m_ghostsScores[i] = 0;
                m_ghostsTicks[i] = 0;
            }
        }

        /// <summary>
        /// We find the total number of pellets.
        /// </summary>
        private void countPellets()
        {
            // We find the number of pellets. We use this to keep track of when 
            // the level has been cleared.
            for (int x = 0; x < m_board.Width; ++x)
            {
                for (int y = 0; y < m_board.Height; ++y)
                {
                    Board.Item item = m_board.Items[x, y];
                    if (item == Board.Item.Pellet
                        ||
                        item == Board.Item.PowerPellet)
                    {
                        m_pelletsRemaining++;
                    }
                }
            }
        }

        #endregion

        #region Constants

        // The number of turns that the power-up lasts for...
        private const int POWER_UP_TURNS = 25;

        // The points scored when Pac-Man eats a standard pellet...
        private const int PELLET_SCORE = 10;

        // The points scored when Pac-Man eats a power pellet...
        private const int POWER_PELLET_SCORE = 50;

        // The points scored when Pac-Man kills a ghost...
        private const int PAC_MAN_KILLS_GHOST_SCORE = 100;

        // Each ghost gets this score when Pac-Man is killed...
        private const int GHOST_KILLS_PACMAN_SCORE = 100;

        // The ghost that killed Pac-Man gets this bonus score
        // in addition to the amount that all ghosts get...
        private const int GHOST_KILLS_PACMAN_BONUS_SCORE = 200;

        // The maximum allowed turns in a game. We finish the game if it goes
        // to this number of turns. If Pac-Man finishes the board, he gets a 
        // bonus based on the number of turns remaining...
        private const int MAX_TURNS_PER_GAME = 1000;

        #endregion

        #region Private data

        // The board. This holds the position of the walls, pellets 
        // and players...
        private Board m_board = new Board();

        // The AI that controls the Pac-Man...
        private IPacManAI m_pacManAI = null;

        // The AIs for the ghosts...
        private IGhostAI[] m_ghostAIs = new IGhostAI[Board.NumberOfGhosts];

        // The Pac-Man's score and time taken...
        private int m_pacManScore = 0;
        private long m_pacManTicks = 0;

        // The score for each ghost. Ghosts earn points when they
        // catch the Pac-Man...
        private int[] m_ghostsScores = new int[Board.NumberOfGhosts];
        private long[] m_ghostsTicks = new long[Board.NumberOfGhosts];

        // The number of lives Pac-Man still has...
        private int m_pacManLivesRemaining = 3;

        // Positions of players before turns were taken...
        private Point m_pacManPositionBeforeTurn;
        private Point[] m_ghostPositionsBeforeTurn = new Point[Board.NumberOfGhosts];

        // The number of remaining pellets.
        // We use this to check when the level has been completed.
        private int m_pelletsRemaining = 0;

        // Holds details about which tiles in the board grid are visible 
        // from which other tiles.
        // For each tile (key in the map), the value is a set of tiles that 
        // are visible from it. Visible means: not obstructed by walls.
        // Only playable tiles are included in the collection.
        private Dictionary<Point, HashSet<Point>> m_mapTileVisibility = new Dictionary<Point,HashSet<Point>>();

        // The AI names of Pac-Man and the ghosts...
        private string m_pacManAIName = "";
        private string[] m_ghostAINames = new string[Board.NumberOfGhosts];

        // The number of turns remaining before we finish the game...
        private int m_turnsRemaining = MAX_TURNS_PER_GAME;

        // True if at least one ghost is scared...
        private bool m_ghostsScared = false;

        // Stopwatch for timing how long AIs take to make their moves...
        private Stopwatch m_stopwatch = new Stopwatch();

        #endregion
    }
}
